Skip to content

feat(tooltip): use the template syntax and rewrite the tooltip component. #3449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from

Conversation

shenjunjian
Copy link
Collaborator

@shenjunjian shenjunjian commented May 21, 2025

PR

使用模板的语法,重写tooltip组件

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Introduced a new, highly customizable Vue 3 tooltip component with support for rich content, configurable delays, placement options, accessibility features, and dynamic appearance.
    • Added a renderless tooltip logic module for advanced tooltip control and integration.
    • Enabled dynamic selection between different tooltip component modes (PC and mobile-first).
    • Added new tooltip event handlers and visibility control functions for improved interaction management.
    • Introduced a reusable timer utility for delayed execution and cleanup in Vue components.
    • Added a Vue directive to detect clicks outside specified elements, enhancing interaction control.
  • Enhancements

    • Improved tooltip event handling and visibility management for a more responsive user experience.
  • Other

    • Updated style import to support LESS in example sites.

Copy link

coderabbitai bot commented May 21, 2025

Walkthrough

This update introduces a new, feature-rich Vue 3 tooltip component with extensive customization options, dynamic template selection, and renderless logic for tooltip state management. Supporting utilities like useTimer and a v-clickoutside directive are added, and exports are updated accordingly. Import statements are adjusted for style and template flexibility. Several test refinements and package metadata reorganizations are included.

Changes

File(s) Change Summary
examples/sites/src/main.js Changed import of custom markdown styles from a CSS to a LESS file.
packages/renderless/src/tooltip/new-index.ts Added new module exporting three functions (handleRefEvent, handlePopEvent, toggleShow) for reactive tooltip event and visibility management.
packages/renderless/src/tooltip/new-vue.ts Introduced a new renderless tooltip logic function (renderless) managing state, timers, and event handling, with a new API export array.
packages/vue-hooks/index.ts Added export for useTimer utility.
packages/vue-hooks/src/useTimer.ts New utility function useTimer for delayed execution with cleanup on component unmount, supporting reactive delay values.
packages/vue/src/tooltip/src/index.ts Replaced dynamic template import with explicit imports of PC and mobile-first tooltip components. Added a template selection function based on mode for dynamic component rendering.
packages/vue/src/tooltip/src/tooltip.vue Added a new Vue 3 tooltip component supporting rich configuration, accessibility, slots, render functions, transitions, and integration with renderless logic and API modules. Includes nested content rendering component and comprehensive prop definitions.
packages/vue/src/tooltip/src/clickoutside.ts Added a new Vue directive v-clickoutside that detects clicks outside an element and triggers a callback, supporting modifiers for mouse event types and managing multiple directive instances globally.
examples/sites/demos/pc/app/tooltip/content-max-height.spec.js Updated tooltip element locator and CSS max-height assertion to target the tooltip element directly with specific text content.
examples/sites/demos/pc/app/tooltip/control.spec.js Refined tooltip content element locators for manual control tests, replaced some hover actions with event dispatches, and commented out previous assertions and interactions.
examples/sites/demos/pc/app/tooltip/popper-options.spec.js Changed test assertion after mouse leave to expect one tooltip element to remain instead of none, clarifying DOM element presence post-interaction.
packages/vue/src/tooltip/package.json Reorganized package metadata: moved license field up, set "type": "module" and "sideEffects": false, reordered and expanded dependencies, and repositioned devDependencies section without content changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ReferenceElement
    participant TooltipComponent
    participant RenderlessLogic
    participant useTimer
    participant Popper
    participant ClickOutsideDirective

    User->>ReferenceElement: mouseenter/mouseleave
    ReferenceElement->>TooltipComponent: emit event
    TooltipComponent->>RenderlessLogic: handleRefEvent(type)
    RenderlessLogic->>useTimer: start/cancel show/hide timer
    useTimer-->>RenderlessLogic: timer fires
    RenderlessLogic->>TooltipComponent: update showPopper state
    TooltipComponent->>Popper: show/hide tooltip (with transition)
    User->>Popper: mouseenter/mouseleave
    Popper->>TooltipComponent: emit event
    TooltipComponent->>RenderlessLogic: handlePopEvent(type)
    RenderlessLogic->>useTimer: manage hide timer
    User->>ClickOutsideDirective: click outside
    ClickOutsideDirective->>TooltipComponent: invoke callback to hide tooltip
Loading

Poem

A tooltip hops with gentle grace,
Showing up just in the right place.
Timers ticking, poppers glide,
Renderless logic by its side.
Click outside, it hides away—
A rabbit’s charm in code today!
🐇🌸✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

examples/sites/demos/pc/app/tooltip/control.spec.js

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

packages/renderless/src/tooltip/new-vue.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

examples/sites/demos/pc/app/tooltip/popper-options.spec.js

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

  • 3 others

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the enhancement New feature or request (功能增强) label May 21, 2025
Copy link

Walkthrough

此PR重写了tooltip组件,使用模板语法以提高可维护性和功能扩展性。引入了新的工具函数和组件,优化了事件处理逻辑,增强了组件的灵活性和性能。

Changes

文件 概要
examples/sites/src/main.js 修改了样式文件的引用,改用.less文件
packages/renderless/src/tooltip/new-index.ts 新增了事件处理函数和显示切换逻辑
packages/renderless/src/tooltip/new-vue.ts 引入了新的钩子和工具函数,重构了tooltip的渲染逻辑
packages/vue-hooks/index.ts 新增了useTimer导出
packages/vue-hooks/src/useTimer.ts 新增了通用定时器函数,支持延时触发和防抖功能
packages/vue/src/tooltip/src/index.ts 修改了模板引用逻辑,支持不同模式的tooltip组件
packages/vue/src/tooltip/src/tooltip.vue 新增了tooltip组件的模板和脚本,支持多种配置和事件处理

(isShow: boolean) => {
// 智能识别模式
if (props.visible === 'auto' && state.referenceElm) {
const { clientWidth, scrollWidth } = state.referenceElm.firstElementChild

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure state.referenceElm.firstElementChild is not null before accessing its properties to avoid potential runtime errors.

}
})

vm.$on('tooltip-update', updatePopper())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure updatePopper() is a function and is correctly invoked to avoid potential runtime errors.

@Issues-translate-bot
Copy link

Bot detected the issue body's language is not English, translate it automatically.


Walkthrough

This PR rewrites the tooltip component, using template syntax for improved maintainability and functional scalability. New tool functions and components are introduced, event processing logic is optimized, and component flexibility and performance are enhanced.

Changes

Documents Summary
examples/sites/src/main.js Modified the reference of the style file and use the .less file instead
packages/renderless/src/tooltip/new-index.ts Added event handler function and display switching logic
packages/renderless/src/tooltip/new-vue.ts New hooks and tool functions were introduced to refactor the rendering logic of tooltip
packages/vue-hooks/index.ts Added useTimer export
packages/vue-hooks/src/useTimer.ts Added a general timer function, supporting delay triggering and anti-shake functions
packages/vue/src/tooltip/src/index.ts Modified template reference logic to support tooltip components with different modes
packages/vue/src/tooltip/src/tooltip.vue Added templates and scripts for tooltip components, supporting multiple configurations and event processing

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/vue/src/tooltip/src/index.ts (1)

3-10: Dynamic template selection implemented

The code now supports dynamic selection between PC and mobile-first tooltip components, replacing the previous virtual template import mechanism. This provides more flexibility for different UI requirements.

Consider translating the Chinese comment "新PC, 老 MF" to English for better international team collaboration.

-const template = (mode) => (mode === 'pc' ? TooltipPc : TooltipMf) // 新PC, 老 MF
+const template = (mode) => (mode === 'pc' ? TooltipPc : TooltipMf) // New PC, old Mobile-First
packages/renderless/src/tooltip/new-vue.ts (1)

73-74: Remove empty teardown hook

onUnmounted(() => {}) is a no-op. Either call doDestroy() to clean Popper
resources or delete the hook to avoid dead code.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1886a5d and 45d814d.

📒 Files selected for processing (7)
  • examples/sites/src/main.js (1 hunks)
  • packages/renderless/src/tooltip/new-index.ts (1 hunks)
  • packages/renderless/src/tooltip/new-vue.ts (1 hunks)
  • packages/vue-hooks/index.ts (1 hunks)
  • packages/vue-hooks/src/useTimer.ts (1 hunks)
  • packages/vue/src/tooltip/src/index.ts (1 hunks)
  • packages/vue/src/tooltip/src/tooltip.vue (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (6)
examples/sites/src/main.js (1)

15-15: CSS to LESS migration: Approve

The import statement has been changed from 'custom-markdown.css' to 'custom-markdown.less', which is consistent with using LESS preprocessor for styling instead of plain CSS.

packages/vue-hooks/src/useTimer.ts (1)

1-36: Well-implemented timer utility with proper cleanup

This is a well-designed Vue hook that provides setTimeout/debounce functionality with automatic cleanup. The implementation:

  • Prevents memory leaks by clearing timers on component unmount
  • Handles reactive delay values
  • Provides clear control methods with good examples
  • Follows Vue Composition API best practices
packages/vue-hooks/index.ts (1)

24-24: Export added for new useTimer hook

Correctly exports the new useTimer hook, making it available for use across the project.

packages/vue/src/tooltip/src/index.ts (1)

104-104:

❓ Verification inconclusive

Template function now passed to setup

The change enables dynamic template selection based on the mode parameter while maintaining the same component API.

Verify that the mode parameter is properly passed to this function in all tooltip usage locations. You might want to check how other components are determining which mode to use.


🏁 Script executed:

#!/bin/bash
# Find where the tooltip component is being imported and used
rg -A 3 "import.*Tooltip" --type ts --type vue

Length of output: 76


🏁 Script executed:

#!/bin/bash
# Search for Tooltip imports in TS and Vue files
rg -A 3 "import .*Tooltip" -g '*.ts' -g '*.vue'

# Search for Tooltip component usage in Vue templates
rg -A 3 "<Tooltip" -g '*.vue'

Length of output: 31792


🏁 Script executed:

#!/bin/bash
# Inspect props definition, setup function, template selection, and mode usage
rg -n -C3 "props:" packages/vue/src/tooltip/src/index.ts
rg -n -C3 "setup(" packages/vue/src/tooltip/src/index.ts
rg -n -C3 "return.*\\$setup" packages/vue/src/tooltip/src/index.ts
rg -n -C3 "mode" packages/vue/src/tooltip/src/index.ts

Length of output: 1012


🏁 Script executed:

#!/bin/bash
# Show top of Vue Tooltip index.ts to inspect imports and $props usage
sed -n '1,30p' packages/vue/src/tooltip/src/index.ts
# Locate $props import and usage
rg -n "\$props" -C3 packages/vue/src/tooltip/src/index.ts

# Show top of renderless Tooltip index.ts to inspect exported props definition
sed -n '1,200p' packages/renderless/src/tooltip/index.ts

Length of output: 6378


Verify that mode prop is actually passed through to the Tooltip template

Ensure that the common $props spread includes a mode property and that when calling $setup({ props, context, template }), the mode value is forwarded into the template(mode) function. In particular:

  • packages/vue/src/tooltip/src/index.ts
    • const template = (mode) => … (around line 10)
    • return $setup({ props, context, template }) (line 104)
  • Review all <Tooltip> usages in your .vue files to confirm whether:
    • A mode prop is set explicitly (e.g. <Tooltip mode="pc" …>), or
    • The default mode from $props correctly resolves (and doesn’t default to mobile unexpectedly).

Please manually verify that every Tooltip consumer receives the intended mode.

packages/renderless/src/tooltip/new-vue.ts (1)

2-2: Suspicious typo: userPopper vs. usePopper

Most Tiny-Vue code and the original Popper composable are named usePopper.
If this really is a typo, both the import and the call on line 14 will throw at runtime.

-import { userPopper, useTimer } from '@opentiny/vue-hooks'
+import { usePopper, useTimer } from '@opentiny/vue-hooks'

…and later:

-const { showPopper, updatePopper, ... } = userPopper({
+const { showPopper, updatePopper, ... } = usePopper({

Please double-check the actual export name in @opentiny/vue-hooks.
If userPopper is indeed intentional, feel free to ignore.

packages/vue/src/tooltip/src/tooltip.vue (1)

15-24: disabled is not in component scope

Template uses !disabled but disabled is a prop that is not returned by
setup(...). Make sure the helper setup(...) proxies all props or return it
explicitly; otherwise Vue will warn “disabled is not defined”.

Comment on lines +5 to +6
export const api = ['state', 'handlePopEvent', 'handleRefEvent']

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

toggleShow omitted from the public API list

tooltip.vue relies on the list in export const api to expose methods returned by
setup(...). toggleShow is created on line 52 but not exported in the array
declared on line 5, making it inaccessible to downstream consumers.

-export const api = ['state', 'handlePopEvent', 'handleRefEvent']
+export const api = [
+  'state',
+  'handlePopEvent',
+  'handleRefEvent',
+  'toggleShow'
+]

Also applies to: 43-53

🤖 Prompt for AI Agents
In packages/renderless/src/tooltip/new-vue.ts around lines 5 to 6 and 43 to 53,
the method toggleShow is defined but not included in the exported api array,
which limits its accessibility to downstream consumers. To fix this, add
'toggleShow' to the api array declared on line 5 so it is properly exposed as
part of the public API.

// 优先级 > effect
type: {
type: String,
validator: (value: string) => Boolean(~['normal', 'warning', 'error', 'info', 'success'].indexOf(value))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用includes代替下吧

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/vue/src/tooltip/src/tooltip.vue (1)

7-8: ⚠️ Potential issue

Fix the accessibility attribute spelling

The attribute :aria-describeby is misspelled and should be :aria-describedby. Screen readers won't recognize the current attribute name, which breaks accessibility.

-:aria-describeby="state.tooltipId"
+:aria-describedby="state.tooltipId"
🧹 Nitpick comments (7)
packages/vue/src/tooltip/src/tooltip.vue (7)

136-136: Improve array inclusion check

Using the bitwise NOT operator with indexOf is an outdated pattern. The includes() method provides a more readable and modern approach, as already used in line 62.

-validator: (value: string) => Boolean(~['normal', 'warning', 'error', 'info', 'success'].indexOf(value))
+validator: (value: string) => ['normal', 'warning', 'error', 'info', 'success'].includes(value)

87-87: Add default value for content prop

The content prop lacks a default value, which could lead to unexpected rendering issues. Consider adding a default empty string.

-content: { type: [String, Object] },
+content: { 
+  type: [String, Object],
+  default: () => ''
+},

101-102: Add default values for boolean props

The manual and modelValue props don't have default values. It's a good practice to explicitly define defaults for boolean props.

-manual: { type: Boolean },
-modelValue: { type: Boolean },
+manual: { 
+  type: Boolean,
+  default: () => false 
+},
+modelValue: { 
+  type: Boolean,
+  default: () => false 
+},

121-123: Define type and default for reference and popper props

The reference and popper props are missing type definitions and default values, making their expected usage unclear.

-reference: {},
-popper: {},
+reference: { 
+  type: [Object, String],
+  default: () => null
+},
+popper: { 
+  type: [Object, String],
+  default: () => null
+},

19-19: Extract complex class binding to a computed property

The class binding combines multiple conditions and is difficult to read. Consider extracting it to a computed property for better maintainability.

// In the template
-:class="['is-' + (type || effect || 'dark'), popperClass, state.showContent ? 'tiny-tooltip__show-tips' : '']"
+:class="popperClassNames"

// In the script, add to the renderless function or setup return
+computed: {
+  popperClassNames() {
+    return [
+      `is-${this.type || this.effect || 'dark'}`,
+      this.popperClass,
+      this.state.showContent ? 'tiny-tooltip__show-tips' : ''
+    ]
+  }
+}

63-67: Improve prop documentation with better comments

Several props have unclear purpose, indicated by Chinese comments or minimal documentation. Consider adding comprehensive JSDoc comments to explain each prop's purpose, expected values, and usage examples.

Example for adjustArrow:

-// 原来未暴露的属性, 自动传入vue-popper
 adjustArrow: {
   type: Boolean,
   default: () => false
 },
+/**
+ * @description Whether to automatically adjust the arrow position
+ * @default false
+ */
+adjustArrow: {
+  type: Boolean,
+  default: () => false
+},

Also applies to: 73-77, 78-82, 120-123, 142-146


20-20: Consider using computed properties for style bindings

Direct style bindings in templates are less maintainable and can make the template harder to read. Consider extracting these to computed properties.

// In the template
-:style="{ ['max-width']: state.tipsMaxWidth }"
+:style="popperStyle"

// In the script, add to the renderless function or setup return
+computed: {
+  popperStyle() {
+    return { 'max-width': this.state.tipsMaxWidth }
+  }
+}

// Same approach for the content wrapper style

Also applies to: 29-29

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45d814d and 9dfb5d9.

📒 Files selected for processing (4)
  • packages/renderless/src/tooltip/new-vue.ts (1 hunks)
  • packages/vue-hooks/src/useTimer.ts (1 hunks)
  • packages/vue/src/tooltip/src/index.ts (1 hunks)
  • packages/vue/src/tooltip/src/tooltip.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/vue/src/tooltip/src/index.ts
  • packages/vue-hooks/src/useTimer.ts
  • packages/renderless/src/tooltip/new-vue.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
examples/sites/demos/pc/app/tooltip/control.spec.js (1)

1-46: 💡 Verification agent

🧩 Analysis chain

Potential test reliability issues with short timeouts.

The test uses very short timeouts (20ms) in several places which could lead to flaky tests, especially in slower environments or during CI/CD pipeline execution.

Consider increasing the timeout values to more reasonable durations (e.g., 100-300ms) or better yet, use Playwright's built-in wait mechanisms like waitForSelector or waitForTimeout with the default timeout values.


🏁 Script executed:

#!/bin/bash
# Count the number of short timeouts in tooltip tests
echo "Checking for potentially flaky timeouts in tooltip tests..."
rg "waitForTimeout\(\s*\d{1,2}\s*\)" examples/sites/demos/pc/app/tooltip/ -l | xargs -I {} echo "Found short timeout in: {}"

Length of output: 801


Increase timeouts or leverage Playwright’s built-in wait mechanisms in tooltip tests

We’ve identified waitForTimeout(20) calls across multiple tooltip spec files, which are prone to flakiness on slower CI agents or under load. Instead of arbitrary short sleeps, prefer explicit waits or increase timeouts to more realistic values (100–300 ms) when you really need them.

Affected files:

  • examples/sites/demos/pc/app/tooltip/control.spec.js
  • examples/sites/demos/pc/app/tooltip/custom-popper.spec.js
  • examples/sites/demos/pc/app/tooltip/content-max-height.spec.js
  • examples/sites/demos/pc/app/tooltip/theme.spec.js
  • examples/sites/demos/pc/app/tooltip/offset.spec.js
  • examples/sites/demos/pc/app/tooltip/delay.spec.js
  • examples/sites/demos/pc/app/tooltip/content.spec.js

Recommendations:

  • Replace await page.waitForTimeout(20) with one of:
    • await page.waitForSelector('<selector>', { state: 'visible' })
    • await expect(locator).toBeVisible({ timeout: 500 })
  • If you must use waitForTimeout, bump it to at least 100–300 ms, or tie it to a variable/config so it can adapt in slower environments.
  • Remove redundant sleeps after actions that already have built-in waiting (e.g., .click(), .hover(), or expect assertions).

This will make your tooltip tests more robust and less prone to intermittent failures.

♻️ Duplicate comments (2)
packages/renderless/src/tooltip/new-vue.ts (2)

5-5: ⚠️ Potential issue

toggleShow is missing from the exported API list

The method toggleShow is used internally (line 40) but not included in the exported API array. This will make it inaccessible to components that rely on this function.

-export const api = ['state', 'handlePopEvent', 'handleRefEvent']
+export const api = ['state', 'handlePopEvent', 'handleRefEvent', 'toggleShow']

29-37: 🛠️ Refactor suggestion

Initialize state with default values instead of overwriting reactive properties

Setting showPopper.value = false directly and then including it in the reactive state could lead to reactivity issues. This approach risks breaking the reactive connection with PopperJS.

-showPopper.value = false // 初始为false
const state = reactive({
-  showPopper,
+  showPopper: false, // Initialize directly in the state object
  popperElm,
  referenceElm,
  tooltipId: guid('tiny-tooltip-', 4),
  showContent: inject('showContent', null),
  tipsMaxWidth: inject('tips-max-width', null)
})
🧹 Nitpick comments (2)
packages/vue/src/tooltip/src/clickoutside.ts (1)

36-56: Use optional chaining for potential null/undefined values

The function accesses properties that might be undefined without using optional chaining, which could lead to runtime errors.

const createDocumentHandler = (el, binding, vnode) =>
  function (mouseup = {}, mousedown = {}) {
-   let popperElm = vnode.context.popperElm || (vnode.context.state && vnode.context.state.popperElm)
+   let popperElm = vnode.context?.popperElm || (vnode.context?.state?.popperElm)

    if (
      !mouseup?.target ||
      !mousedown?.target ||
      el.contains(mouseup.target) ||
      el.contains(mousedown.target) ||
      el === mouseup.target ||
      (popperElm && (popperElm.contains(mouseup.target) || popperElm.contains(mousedown.target)))
    ) {
      return
    }

    if (binding.expression && el[nameSpace].methodName && vnode.context[el[nameSpace].methodName]) {
      vnode.context[el[nameSpace].methodName]()
    } else {
-     el[nameSpace].bindingFn && el[nameSpace].bindingFn()
+     el[nameSpace]?.bindingFn?.()
    }
  }
🧰 Tools
🪛 Biome (1.9.4)

[error] 38-38: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 54-54: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

examples/sites/demos/pc/app/tooltip/control.spec.js (1)

12-15: Improved selector specificity for tooltip elements.

The change from text-based selectors to more precise class-based selectors with index targeting improves test stability and clarity. Each tooltip element now has a specific locator that directly corresponds to different tooltip modes.

Consider using more descriptive variable names like intelligentLongTooltip, intelligentShortTooltip, etc. instead of content1, content2, etc. to improve readability and make the test more self-documenting.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be4044f and c5c7e3e.

📒 Files selected for processing (7)
  • examples/sites/demos/pc/app/tooltip/content-max-height.spec.js (1 hunks)
  • examples/sites/demos/pc/app/tooltip/control.spec.js (2 hunks)
  • examples/sites/demos/pc/app/tooltip/popper-options.spec.js (1 hunks)
  • packages/renderless/src/tooltip/new-vue.ts (1 hunks)
  • packages/vue/src/tooltip/package.json (1 hunks)
  • packages/vue/src/tooltip/src/clickoutside.ts (1 hunks)
  • packages/vue/src/tooltip/src/tooltip.vue (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/vue/src/tooltip/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue/src/tooltip/src/tooltip.vue
🧰 Additional context used
🧬 Code Graph Analysis (1)
examples/sites/demos/pc/app/tooltip/control.spec.js (1)
examples/sites/demos/pc/app/tooltip/content-max-height.spec.js (1)
  • preview (7-7)
🪛 Biome (1.9.4)
packages/vue/src/tooltip/src/clickoutside.ts

[error] 38-38: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 54-54: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (8)
packages/renderless/src/tooltip/new-vue.ts (2)

39-43: Good use of timer utility functions

The implementation of delayed showing/hiding using the useTimer hook is well-structured. It cleanly separates timer logic and provides good control over tooltip behavior.


72-76: Update event handler registration to avoid immediate execution

The event handler for 'tooltip-update' is properly implemented as a callback function, ensuring it's only executed when the event is triggered.

packages/vue/src/tooltip/src/clickoutside.ts (1)

96-115: Efficient element removal from nodeList

The code efficiently removes elements from the nodeList array by iterating through it and splicing when a match is found. It also properly cleans up references by deleting the nameSpace property from the element.

examples/sites/demos/pc/app/tooltip/content-max-height.spec.js (2)

9-9: Improved test selector for tooltip element

The updated selector uses text content to identify the tooltip element, which is more reliable than the previous approach. This matches the new template-based implementation of the tooltip component.


14-14: Updated CSS assertion target

The CSS max-height assertion now targets the tooltip element directly rather than a nested element. This reflects changes in the component's DOM structure and style application.

examples/sites/demos/pc/app/tooltip/popper-options.spec.js (1)

14-14:

✅ Verification successful

Test assertion updated to match new component behavior

The test now expects the tooltip element to remain in the DOM (count=1) after mouse movement, whereas previously it expected the element to be removed (count=0). This change aligns with the new implementation where the tooltip may remain in the DOM but become hidden when not active.

Please verify that this change is intentional and matches the expected behavior of the new tooltip implementation. The comment suggests that the DOM element is only removed when the component unmounts, not when the tooltip becomes inactive.


🏁 Script executed:

#!/bin/bash
# Check how tooltip visibility is managed in the new implementation
rg -A 3 -B 3 "showPopper" --glob "packages/renderless/src/tooltip/**"

Length of output: 5948


Test assertion aligns with new tooltip behavior

The renderless tooltip logic now only toggles state.showPopper to hide/show the popper, and does not remove its DOM node until the component unmounts. Therefore expecting a count of 1 in the spec is correct.

No changes needed.

examples/sites/demos/pc/app/tooltip/control.spec.js (2)

17-17: LGTM - Selecting tooltip popup with text content.

This selector appropriately targets the tooltip popup text content.


42-42: LGTM - Using Playwright's hover method.

Using the .hover() method is more idiomatic for Playwright and improves readability.

Comment on lines +45 to +54
Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider including toggleShow in the API object

While toggleShowFn is used internally to handle visibility, it's not exposed in the API object, limiting component flexibility.

Object.assign(api, {
  state,
  delayShow,
  cancelDelayShow,
  delayHide,
  cancelDelayHide,
  delayHideAfter,
+ toggleShow: toggleShowFn,
  handlePopEvent: handlePopEvent({ props, api }),
  handleRefEvent: handleRefEvent({ props, api })
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
toggleShow: toggleShowFn,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
🤖 Prompt for AI Agents
In packages/renderless/src/tooltip/new-vue.ts around lines 45 to 54, the
toggleShowFn function is used internally but not included in the exported API
object, which limits external control over tooltip visibility. To fix this, add
toggleShowFn to the Object.assign call that defines the api object properties,
exposing it as toggleShow or a similarly named property to allow components
using this API to toggle visibility directly.

Comment on lines +13 to +34
import { on, isServer } from '@opentiny/utils'

const nodeList = []
const nameSpace = '@@clickoutsideContext'
let startClick
let seed = 0

if (!isServer) {
on(document, 'mousedown', (event) => {
startClick = event
nodeList
.filter((node) => node[nameSpace].mousedownTrigger)
.forEach((node) => node[nameSpace].documentHandler(event, startClick))
})

on(document, 'mouseup', (event) => {
nodeList
.filter((node) => !node[nameSpace].mousedownTrigger)
.forEach((node) => node[nameSpace].documentHandler(event, node[nameSpace]?.mouseupTrigger ? event : startClick))
startClick = null
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure document event listeners are properly cleaned up

The implementation attaches global document event listeners, but there's no mechanism to remove these listeners when they're no longer needed. This could lead to memory leaks if the application repeatedly mounts and unmounts components using this directive.

Consider adding a mechanism to remove document event listeners when no elements are using the directive:

if (!isServer) {
+ let hasAttachedEvents = false;
+ const attachEvents = () => {
+   if (hasAttachedEvents) return;
+   hasAttachedEvents = true;
    on(document, 'mousedown', (event) => {
      startClick = event
      nodeList
        .filter((node) => node[nameSpace].mousedownTrigger)
        .forEach((node) => node[nameSpace].documentHandler(event, startClick))
    })

    on(document, 'mouseup', (event) => {
      nodeList
        .filter((node) => !node[nameSpace].mousedownTrigger)
        .forEach((node) => node[nameSpace].documentHandler(event, node[nameSpace]?.mouseupTrigger ? event : startClick))
      startClick = null
    })
+ }
+ 
+ const detachEvents = () => {
+   if (!hasAttachedEvents || nodeList.length > 0) return;
+   hasAttachedEvents = false;
+   document.removeEventListener('mousedown', /* stored handler reference */);
+   document.removeEventListener('mouseup', /* stored handler reference */);
+ }
+
+ attachEvents();
}

Then call detachEvents() in the unbind hook when nodeList.length === 0.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/vue/src/tooltip/src/clickoutside.ts between lines 13 and 34, the
global document event listeners for 'mousedown' and 'mouseup' are added but
never removed, risking memory leaks. Implement a detachEvents function that
removes these event listeners from the document. Then, in the directive's unbind
hook, check if nodeList is empty and call detachEvents to clean up the listeners
when no elements use the directive.

Comment on lines +22 to +30
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)

await visibleSwitch.click()
await preview.getByText('内容不超长').hover()
await expect(pop1).toBeHidden()
// await visibleSwitch.click()
// await page.waitForTimeout(20)
// await content2.dispatchEvent('mouseleave')
// await page.waitForTimeout(20)
// await expect(pop1).toBeHidden()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplified tooltip interaction with direct event dispatching.

Using dispatchEvent('mouseenter') directly on the content locator is a good approach for testing hover interaction. However, there's a significant amount of commented-out code that should be addressed.

Please either remove the commented-out code (lines 26-30) or add a comment explaining why it's retained. Commented-out code can lead to confusion for future maintainers.

await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)

-// await visibleSwitch.click()
-// await page.waitForTimeout(20)
-// await content2.dispatchEvent('mouseleave')
-// await page.waitForTimeout(20)
-// await expect(pop1).toBeHidden()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)
await visibleSwitch.click()
await preview.getByText('内容不超长').hover()
await expect(pop1).toBeHidden()
// await visibleSwitch.click()
// await page.waitForTimeout(20)
// await content2.dispatchEvent('mouseleave')
// await page.waitForTimeout(20)
// await expect(pop1).toBeHidden()
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)
🤖 Prompt for AI Agents
In examples/sites/demos/pc/app/tooltip/control.spec.js around lines 22 to 30,
there is commented-out code related to tooltip interaction that is no longer
needed. To improve code clarity and maintainability, either remove these
commented lines entirely or add a clear comment explaining why this code is kept
for future reference. This will prevent confusion for future maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request (功能增强)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants